package edu.hawaii.ics314.ical.model;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.GregorianCalendar;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

import edu.hawaii.ics314.ical.model.CalendarComponent.ComponentPriority;
import edu.hawaii.ics314.ical.model.DateTime.DateTimeComponent;

public class Calendars {
	
	/**
	 * Calculates free time between the specified calendar, excluding events below the
	 * <code>minPriorityThreshold</code>.
	 * 
	 * @param calendar - the specified calendar.
	 * @param minPriorityThreshold - the minimum priority below which will be excluded from consideration.
	 * @return A calendar containing events representing free time in the specified calendar.
	 */
	public static Calendar freeTime(Calendar calendar, ComponentPriority minPriorityThreshold) {
		Objects.requireNonNull(calendar);
		ArrayList<Event> eventsList = new ArrayList<Event>();
		eventsList.addAll(Arrays.asList(calendar.getCalendarComponents(Event.class)));
		// If we are excluding low priority events, remove them before starting. 
		if(minPriorityThreshold != ComponentPriority.UNDEFINED) {
			int minPriIntValue = minPriorityThreshold.intValue();
			for(int i = 0; i < eventsList.size(); i++) {
				if(eventsList.get(i).getPriority().intValue() > minPriIntValue) {
					eventsList.remove(i);
					i--;
				}
			}
		}
		Event[] events = eventsList.toArray(new Event[eventsList.size()]);
		Arrays.sort(events); // Sort events by start time.
		ArrayList<TimeSpan> busyRuns = new ArrayList<TimeSpan>();
		// Find TimeSpan "runs" which span multiple events. 
		for(Event event : events) {
			TimeSpan evtTimeSpan = new TimeSpan(event.getStartDateTime(), event.getEndDateTime());
			boolean addedToExistingRun = false;
			for(int i = 0; i < busyRuns.size(); i++) {
				// If the event time span intersects with the current busy run, replace it with the union of the two.
				TimeSpan existingRun = busyRuns.get(i);
				if(existingRun.intersection(evtTimeSpan) != null) {
					TimeSpan extendedBusyRun = existingRun.union(evtTimeSpan);
					busyRuns.set(i, extendedBusyRun);
					addedToExistingRun = true;
					break;
				}
			}
			// If no existing run intersects with the current event time span, add it as a new busy run.
			if(!addedToExistingRun) {
				busyRuns.add(evtTimeSpan);
			}
		}
		// Remove any empty runs. 
		for(int i = 0; i < busyRuns.size(); i++) {
			if(busyRuns.get(i).toLong(TimeUnit.MILLISECONDS) <= 0) {
				busyRuns.remove(i);
				i--;
			}
		}
		// Since busy runs are already sorted by start time, find times between them (free times).
		ArrayList<TimeSpan> freeRuns = new ArrayList<TimeSpan>();
		if(busyRuns.isEmpty()) {
			
		} else {
			GregorianCalendar gregorianCal = new GregorianCalendar();
			DateTime dtStart = busyRuns.get(0).getStartDateTime();
			DateTime dtEnd = busyRuns.get(busyRuns.size() - 1).getEndDateTime();
			if(!dtStart.isMidnight()) { // Free time starts at midnight of the day before the first event.
				int dtStartDay = dtStart.getComponentValue(DateTimeComponent.DAY);
				int dtStartMonth = dtStart.getComponentValue(DateTimeComponent.MONTH);
				int dtStartYear = dtStart.getComponentValue(DateTimeComponent.YEAR);
				// If it is the first day of the month, use first day of previous month.
				if(dtStart.getComponentValue(DateTimeComponent.DAY) < 1) {
					dtStartMonth--;
					// If it is the first month of the year, use December of the previous year.
					if(dtStartMonth < 1) {
						dtStartYear--;
						dtStartMonth = 12;
					}
					// Now that we have found the month and year we will be using, we can find the last day of month.
					gregorianCal.set(GregorianCalendar.YEAR, dtStartYear);
					gregorianCal.set(GregorianCalendar.MONTH, dtStartMonth);
					dtStartDay = gregorianCal.getActualMaximum(GregorianCalendar.DAY_OF_MONTH);
				}
				freeRuns.add(new TimeSpan(new DateTime(dtStartYear, dtStartMonth, dtStartDay, 0, 0, 0), dtStart));
			}
			for(int i = 1; i < busyRuns.size(); i++) {
				freeRuns.add(new TimeSpan(busyRuns.get(i - 1).getEndDateTime(), busyRuns.get(i).getStartDateTime()));
			}
			if(!dtEnd.isMidnight()) { // Free time ends on midnight on the day after the date of the last event.
				int dtEndYear = dtEnd.getComponentValue(DateTimeComponent.YEAR);
				int dtEndMonth = dtEnd.getComponentValue(DateTimeComponent.MONTH);
				int dtEndDay = dtEnd.getComponentValue(DateTimeComponent.DAY) + 1;
				// Check if the end day is after the last day of the month;
				gregorianCal.set(GregorianCalendar.YEAR, dtEndYear);
				gregorianCal.set(GregorianCalendar.MONTH, dtEndMonth);
				if(dtEndDay > gregorianCal.getActualMaximum(GregorianCalendar.DAY_OF_MONTH)) {
					dtEndDay = 1;
					dtEndMonth++;
					if(dtEndMonth > 12) {
						dtEndMonth = 1;
						dtEndYear++;
					}
				}
				freeRuns.add(new TimeSpan(dtEnd, new DateTime(dtEndYear, dtEndMonth, dtEndDay, 0, 0, 0)));
			}
		}
		// Add the free runs to a calendar as events.
		CalendarComponentInfo freeTimeCalInfo = new CalendarComponentInfo(calendar);
		freeTimeCalInfo.summary = new Property<String>("SUMMARY", "Free Time");
		Calendar freeTimeCal = new Calendar(new Property<String>("VERSION", calendar.getVersion()), freeTimeCalInfo);
		for(TimeSpan freeRun : freeRuns) {
			CalendarComponentInfo freeRunEvtInfo = new CalendarComponentInfo();
			freeRunEvtInfo.hostCalendar = freeTimeCal;
			// TODO: Find a better way to set the summary which doesn't require hard-coding it.
			freeRunEvtInfo.summary = new Property<String>("SUMMARY", "Free Time");
			freeRunEvtInfo.startDateTime = new Property<DateTime>("DTSTART", freeRun.getStartDateTime());
			freeRunEvtInfo.endDateTime = new Property<DateTime>("DTEND", freeRun.getEndDateTime());
			freeTimeCal.addCalendarComponent(new Event(freeRunEvtInfo));
		}
		return freeTimeCal;
	}
	
	/**
	 * The intersection between the events in the specified calendars.
	 * 
	 * @param first - the first calendar.
	 * @param second - the second calendar.
	 * @return A <code>Calendar</code> containing the intersection of the specified calendars.
	 */
	public static Calendar intersection(Calendar first, Calendar second) {
		Objects.requireNonNull(first);
		Objects.requireNonNull(second);
		// TODO: Enforce same time zone requirement.
		// Sort events by start DateTime.
		Event[] firstCalEvts = first.getCalendarComponents(Event.class);
		Event[] secondCalEvts = second.getCalendarComponents(Event.class);
		Arrays.sort(firstCalEvts);
		Arrays.sort(secondCalEvts);
		// Find all intersections between all events. 
		ArrayList<TimeSpan> intersections = new ArrayList<TimeSpan>();
		for(Event firstEvt : firstCalEvts) {
			TimeSpan firstTimeSpan = new TimeSpan(firstEvt.getStartDateTime(), firstEvt.getEndDateTime());
			for(Event secondEvt : secondCalEvts) {
				TimeSpan secondTimeSpan = new TimeSpan(secondEvt.getStartDateTime(), secondEvt.getEndDateTime());
				TimeSpan intersection = firstTimeSpan.intersection(secondTimeSpan);
				if(intersection != null && intersection.toLong(TimeUnit.MILLISECONDS) > 0) {
					intersections.add(intersection);
				}
			}
		}
		// Union any intersections which overlap.
		if(!intersections.isEmpty()) {
			Collections.sort(intersections);
			TimeSpan curIntersectRun = intersections.get(0);
			for(int i = 1; i < intersections.size(); i++) {
				if(curIntersectRun.overlaps(intersections.get(i))) {
					curIntersectRun = curIntersectRun.union(intersections.get(i));
					intersections.set(i - 1, curIntersectRun);
					intersections.remove(i);
					i--;
				} else {
					curIntersectRun = intersections.get(i);
				}
			}
		}
		// Add the time spans to a calendar as events.
		CalendarComponentInfo intersectCalInfo = new CalendarComponentInfo(first);
		intersectCalInfo.summary = new Property<String>("SUMMARY", "POSSIBLE MEETING TIMES");
		Calendar intersectCal = new Calendar(new Property<String>("VERSION", first.getVersion()), intersectCalInfo);
		for(TimeSpan intersection : intersections) {
			CalendarComponentInfo intersectEvtInfo = new CalendarComponentInfo();
			intersectEvtInfo.hostCalendar = intersectCal;
			intersectEvtInfo.startDateTime = new Property<DateTime>("DTSTART", intersection.getStartDateTime());
			intersectEvtInfo.endDateTime = new Property<DateTime>("DTEND", intersection.getEndDateTime());
			// TODO: Find a better way to set the summary which doesn't require hard-coding it.
			intersectEvtInfo.summary = new Property<String>("SUMMARY", "POSSIBLE MEETING TIME"); 
			intersectCal.addCalendarComponent(new Event(intersectEvtInfo));
		}
		return intersectCal;
	}
	
}
